internaltool-mcp 1.6.24 → 1.6.26
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 +382 -4
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -391,11 +391,16 @@ Set confirmed=false first to preview, then confirmed=true to execute everything.
|
|
|
391
391
|
// ── Save park note + server-side comment & notifications ──────────────
|
|
392
392
|
await api.patch(`/api/tasks/${taskId}/park`, { summary, remaining, blockers })
|
|
393
393
|
|
|
394
|
-
// ── Delete cursor rules file
|
|
394
|
+
// ── Delete cursor rules file + task-scoped workspace files ───────────────
|
|
395
395
|
let cursorRulesCleared = null
|
|
396
396
|
if (task?.cursorRules?.trim()) {
|
|
397
397
|
cursorRulesCleared = deleteCursorRulesFile(task.key, repoPath)
|
|
398
398
|
}
|
|
399
|
+
deleteCursorWorkspace(task?.agentRole || null, repoPath)
|
|
400
|
+
// Mark workspace as cleared in DB so UI shows "not active"
|
|
401
|
+
api.patch(`/api/tasks/${taskId}`, {
|
|
402
|
+
agentWorkspace: { clearedAt: new Date().toISOString() }
|
|
403
|
+
}).catch(() => {/* non-fatal */})
|
|
399
404
|
|
|
400
405
|
// ── #9 Capture last commit for handoff metadata ───────────────────────
|
|
401
406
|
const lastCommit = getLastCommitMeta(repoRoot)
|
|
@@ -438,6 +443,7 @@ Set confirmed=false first to read everything, then confirmed=true to execute.`,
|
|
|
438
443
|
.describe('Set the agent role for this task session. Role-specific behavioral constraints are injected into cursor rules.'),
|
|
439
444
|
},
|
|
440
445
|
async ({ taskId, confirmed = false, repoPath, autoStash = false, agentRole }) => {
|
|
446
|
+
trackTaskActivity(taskId, 'unpark_task')
|
|
441
447
|
const taskRes = await api.get(`/api/tasks/${taskId}`)
|
|
442
448
|
const task = taskRes?.data?.task
|
|
443
449
|
const branch = task?.github?.headBranch || null
|
|
@@ -613,6 +619,7 @@ Roles: BUILDER agents MUST call this before editing. Coordinators call this as p
|
|
|
613
619
|
files: z.array(z.string()).min(1).describe('List of file paths this task will exclusively edit (e.g. ["server/routes/tasks.js", "client/src/App.jsx"])'),
|
|
614
620
|
},
|
|
615
621
|
async ({ taskId, files }) => {
|
|
622
|
+
trackTaskActivity(taskId, 'claim_files')
|
|
616
623
|
const res = await api.post(`/api/tasks/${taskId}/files/claim`, { files })
|
|
617
624
|
if (!res?.success) {
|
|
618
625
|
if (res?.conflicts) {
|
|
@@ -693,6 +700,7 @@ Call confirmed=false to preview the decomposition, confirmed=true to save it.`,
|
|
|
693
700
|
confirmed: z.boolean().optional().default(false).describe('Set true to save the decomposition and create subtasks on the board'),
|
|
694
701
|
},
|
|
695
702
|
async ({ taskId, subtaskPlan, confirmed = false }) => {
|
|
703
|
+
trackTaskActivity(taskId, 'decompose_task')
|
|
696
704
|
const taskRes = await apiWithRetry(() => api.get(`/api/tasks/${taskId}`))
|
|
697
705
|
if (!taskRes?.success) return errorText('Task not found')
|
|
698
706
|
const task = taskRes.data.task
|
|
@@ -820,6 +828,7 @@ Scouts MUST NOT modify any source code files or create branches.`,
|
|
|
820
828
|
report: z.string().optional().default('').describe('Your structured scout findings in markdown (required when confirmed=true)'),
|
|
821
829
|
},
|
|
822
830
|
async ({ taskId, confirmed = false, report = '' }) => {
|
|
831
|
+
trackTaskActivity(taskId, 'scout_task')
|
|
823
832
|
const taskRes = await apiWithRetry(() => api.get(`/api/tasks/${taskId}`))
|
|
824
833
|
if (!taskRes?.success) return errorText('Task not found')
|
|
825
834
|
const task = taskRes.data.task
|
|
@@ -891,6 +900,7 @@ Returns systemPrompt ready to use as a Claude system prompt.`,
|
|
|
891
900
|
.describe('Agent role for this session. Falls back to task.agentRole if omitted.'),
|
|
892
901
|
},
|
|
893
902
|
async ({ taskId, role }) => {
|
|
903
|
+
trackTaskActivity(taskId, 'get_agent_context')
|
|
894
904
|
const qs = role ? `?role=${role}` : ''
|
|
895
905
|
const res = await apiWithRetry(() => api.get(`/api/tasks/${taskId}/agent-context${qs}`))
|
|
896
906
|
if (!res?.success) return errorText(res?.message || 'Failed to get agent context')
|
|
@@ -1500,6 +1510,152 @@ Flow:
|
|
|
1500
1510
|
})
|
|
1501
1511
|
}
|
|
1502
1512
|
)
|
|
1513
|
+
|
|
1514
|
+
// ── log_session_event ─────────────────────────────────────────────────────────
|
|
1515
|
+
server.tool(
|
|
1516
|
+
'log_session_event',
|
|
1517
|
+
`Log a skill invocation, subagent start, rule activation, or informational note to the task's session timeline.
|
|
1518
|
+
Call this whenever you:
|
|
1519
|
+
- Invoke a skill (type="skill", name=skill file name e.g. "scout-codebase")
|
|
1520
|
+
- Spawn a subagent (type="subagent", name=subagent name)
|
|
1521
|
+
- Activate a rule (type="rule", name=rule name)
|
|
1522
|
+
- Want to note a significant step (type="info", name=short label)
|
|
1523
|
+
The log is visible in InternalTool under the task's Session tab — gives the human full visibility into what the agent is doing.`,
|
|
1524
|
+
{
|
|
1525
|
+
taskId: z.string().describe("Task's MongoDB ObjectId"),
|
|
1526
|
+
type: z.enum(['skill', 'subagent', 'rule', 'info']).describe('Event type'),
|
|
1527
|
+
name: z.string().describe('Name of the skill / subagent / rule / step'),
|
|
1528
|
+
summary: z.string().optional().default('').describe('Optional one-line description of what this event does'),
|
|
1529
|
+
role: z.string().optional().describe('Active agent role at this point (builder, scout, reviewer, coordinator)'),
|
|
1530
|
+
},
|
|
1531
|
+
async ({ taskId, type, name, summary = '', role }) => {
|
|
1532
|
+
trackTaskActivity(taskId, 'log_session_event', { role, summary: `[${type}] ${name}` })
|
|
1533
|
+
// Also push a dedicated entry with the correct event type
|
|
1534
|
+
api.post(`/api/tasks/${taskId}/session/log`, {
|
|
1535
|
+
type, name, role: role || null, summary,
|
|
1536
|
+
}).catch(() => {})
|
|
1537
|
+
return text({ logged: true, type, name, summary })
|
|
1538
|
+
}
|
|
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
|
+
)
|
|
1503
1659
|
}
|
|
1504
1660
|
|
|
1505
1661
|
// ── Standup activity formatter ────────────────────────────────────────────────
|
|
@@ -1998,6 +2154,7 @@ Use this when a developer says "start task", "brief me on", or "what do I need t
|
|
|
1998
2154
|
.describe('Set the agent role for this task session. Role-specific behavioral constraints are injected into cursor rules. builder=implements code, scout=reads/analyzes only, reviewer=reviews PRs only, coordinator=decomposes work.'),
|
|
1999
2155
|
},
|
|
2000
2156
|
async ({ taskId, confirmed = false, repoPath, agentRole }) => {
|
|
2157
|
+
trackTaskActivity(taskId, 'kickoff_task')
|
|
2001
2158
|
const taskRes = await api.get(`/api/tasks/${taskId}`)
|
|
2002
2159
|
if (!taskRes?.success) return errorText('Task not found')
|
|
2003
2160
|
const task = taskRes.data.task
|
|
@@ -2028,6 +2185,7 @@ Use this when a developer says "start task", "brief me on", or "what do I need t
|
|
|
2028
2185
|
// ── Preview: show the full plan before touching anything ──
|
|
2029
2186
|
const pendingApv = (task.approvals || []).find(a => a.state === 'pending') || null
|
|
2030
2187
|
const hasApprovedApv = (task.approvals || []).some(a => a.state === 'approved')
|
|
2188
|
+
const approvalState = pendingApv ? 'pending' : hasApprovedApv ? 'approved' : 'none'
|
|
2031
2189
|
const approvalBlocks = ['backlog', 'todo'].includes(task.column) && !hasApprovedApv
|
|
2032
2190
|
|
|
2033
2191
|
// Build the next-step roadmap so developer knows exactly what comes after reading the plan
|
|
@@ -2227,11 +2385,18 @@ Use this when a developer says "start task", "brief me on", or "what do I need t
|
|
|
2227
2385
|
|
|
2228
2386
|
// ── Confirmed: move to in_progress and fetch recent commits ──
|
|
2229
2387
|
let recentCommits = []
|
|
2388
|
+
let projectAgentConfig = null
|
|
2230
2389
|
try {
|
|
2231
2390
|
const commitsRes = await api.get(`/api/projects/${task.project}/github/commits?per_page=10`)
|
|
2232
2391
|
if (commitsRes?.success) recentCommits = commitsRes.data.commits || []
|
|
2233
2392
|
} catch { /* GitHub may not be linked */ }
|
|
2234
2393
|
|
|
2394
|
+
// Fetch project agentConfig for custom role generation
|
|
2395
|
+
try {
|
|
2396
|
+
const projRes = await api.get(`/api/projects/${task.project}`)
|
|
2397
|
+
if (projRes?.success) projectAgentConfig = projRes.data.project?.agentConfig || null
|
|
2398
|
+
} catch { /* non-fatal */ }
|
|
2399
|
+
|
|
2235
2400
|
// If the task already has a branch linked, it's safe to move to in_progress now.
|
|
2236
2401
|
// If not, create_branch will do the move once the branch is created.
|
|
2237
2402
|
const alreadyHasBranch = !!task.github?.headBranch
|
|
@@ -2252,10 +2417,21 @@ Use this when a developer says "start task", "brief me on", or "what do I need t
|
|
|
2252
2417
|
cursorRulesFile = writeCursorRulesFile(task.key, '(No task-specific rules — follow role constraints above.)', repoPath, agentRole)
|
|
2253
2418
|
}
|
|
2254
2419
|
|
|
2255
|
-
//
|
|
2256
|
-
|
|
2257
|
-
|
|
2420
|
+
// Dynamically generate .cursor/agents, .cursor/skills, .cursor/commands
|
|
2421
|
+
// based on live task data — so every agent gets the right task ID, files, and role
|
|
2422
|
+
const workspaceResult = writeCursorWorkspace(task, projectAgentConfig, repoPath || process.cwd())
|
|
2423
|
+
|
|
2424
|
+
// Persist agentRole + workspace status to server
|
|
2425
|
+
const workspacePatch = {
|
|
2426
|
+
...(agentRole ? { agentRole } : {}),
|
|
2427
|
+
agentWorkspace: {
|
|
2428
|
+
kickedOffAt: new Date().toISOString(),
|
|
2429
|
+
clearedAt: null,
|
|
2430
|
+
role: agentRole || task.agentRole || null,
|
|
2431
|
+
files: workspaceResult?.written?.map(f => f.replace(workspaceResult.repoRoot, '').replace(/^\//, '')) || [],
|
|
2432
|
+
},
|
|
2258
2433
|
}
|
|
2434
|
+
api.patch(`/api/tasks/${taskId}`, workspacePatch).catch(() => {/* non-fatal */})
|
|
2259
2435
|
|
|
2260
2436
|
return text({
|
|
2261
2437
|
started: {
|
|
@@ -2282,6 +2458,9 @@ Use this when a developer says "start task", "brief me on", or "what do I need t
|
|
|
2282
2458
|
cursorRulesFile: cursorRulesFile
|
|
2283
2459
|
? { written: true, path: cursorRulesFile, note: 'Rules file written to your repo. Cursor enforces it automatically on every prompt.' }
|
|
2284
2460
|
: (hasCursorRules || agentRole) ? { written: false, note: 'Could not write rules file — not inside a git repo.' } : null,
|
|
2461
|
+
cursorWorkspace: workspaceResult
|
|
2462
|
+
? { written: true, files: workspaceResult.written.map(f => f.replace(workspaceResult.repoRoot, '')), note: 'Agent, skill, and command files generated dynamically from task data. Reload Cursor Settings → Rules, Skills, Subagents to see them.' }
|
|
2463
|
+
: { written: false, note: 'Could not generate workspace files — not inside a git repo.' },
|
|
2285
2464
|
recentCommits: recentCommits.slice(0, 5).map(c => ({
|
|
2286
2465
|
sha: c.sha?.slice(0, 7),
|
|
2287
2466
|
message: c.commit?.message?.split('\n')[0],
|
|
@@ -2824,6 +3003,202 @@ function writeCursorRulesFile(taskKey, rulesMarkdown, startPath, role = null) {
|
|
|
2824
3003
|
}
|
|
2825
3004
|
}
|
|
2826
3005
|
|
|
3006
|
+
/**
|
|
3007
|
+
* Dynamically generate .cursor/ workspace files from live InternalTool data.
|
|
3008
|
+
*
|
|
3009
|
+
* Sources (in priority order):
|
|
3010
|
+
* 1. project.agentConfig.subagents[] → .cursor/agents/<name>.md
|
|
3011
|
+
* 2. project.agentConfig.skills[] → .cursor/skills/<name>.md
|
|
3012
|
+
* 3. project.agentConfig.rules[] → .cursor/rules/<name>.mdc (project-level, alwaysApply)
|
|
3013
|
+
* 4. Built-in ROLE_RULES / default skill templates (fallback when DB arrays are empty)
|
|
3014
|
+
*
|
|
3015
|
+
* Task-specific agent file (.cursor/agents/active-agent.md) is always written
|
|
3016
|
+
* from the task's role + claimed files + task ID so Cursor knows exactly what to do.
|
|
3017
|
+
* It is deleted by deleteCursorWorkspace() when the task is parked or completed.
|
|
3018
|
+
*/
|
|
3019
|
+
function writeCursorWorkspace(task, projectAgentConfig, startPath) {
|
|
3020
|
+
try {
|
|
3021
|
+
const repoRoot = findRepoRoot(startPath)
|
|
3022
|
+
if (!repoRoot) return null
|
|
3023
|
+
|
|
3024
|
+
const role = task.agentRole || null
|
|
3025
|
+
const taskId = String(task._id)
|
|
3026
|
+
const taskKey = task.key || 'TASK-???'
|
|
3027
|
+
const taskTitle = task.title || ''
|
|
3028
|
+
const claimedFiles = task.claimedFiles || []
|
|
3029
|
+
const hasScout = !!(task.scoutReport?.trim())
|
|
3030
|
+
const hasDec = !!(task.decomposition?.trim())
|
|
3031
|
+
const cfg = projectAgentConfig || {}
|
|
3032
|
+
|
|
3033
|
+
const agentsDir = join(repoRoot, '.cursor', 'agents')
|
|
3034
|
+
const skillsDir = join(repoRoot, '.cursor', 'skills')
|
|
3035
|
+
const commandsDir = join(repoRoot, '.cursor', 'commands')
|
|
3036
|
+
const rulesDir = join(repoRoot, '.cursor', 'rules')
|
|
3037
|
+
mkdirSync(agentsDir, { recursive: true })
|
|
3038
|
+
mkdirSync(skillsDir, { recursive: true })
|
|
3039
|
+
mkdirSync(commandsDir, { recursive: true })
|
|
3040
|
+
mkdirSync(rulesDir, { recursive: true })
|
|
3041
|
+
|
|
3042
|
+
const written = []
|
|
3043
|
+
|
|
3044
|
+
// ── 1. Project-level rules from DB → .cursor/rules/<name>.mdc ─────────────
|
|
3045
|
+
const projectRules = cfg.rules || []
|
|
3046
|
+
for (const r of projectRules) {
|
|
3047
|
+
if (!r.name || !r.body) continue
|
|
3048
|
+
const content = `---\ndescription: ${r.description || r.name} — project rule managed in InternalTool. Do not edit manually.\nalwaysApply: ${r.alwaysApply ? 'true' : 'false'}\n---\n\n${r.body}\n`
|
|
3049
|
+
const f = join(rulesDir, `${r.name}.mdc`)
|
|
3050
|
+
writeFileSync(f, content, 'utf8')
|
|
3051
|
+
written.push(f)
|
|
3052
|
+
}
|
|
3053
|
+
|
|
3054
|
+
// ── 2. Subagents from DB → .cursor/agents/<name>.md ───────────────────────
|
|
3055
|
+
// If DB has subagents defined, write them verbatim (admin-authored markdown)
|
|
3056
|
+
const dbSubagents = cfg.subagents || []
|
|
3057
|
+
for (const s of dbSubagents) {
|
|
3058
|
+
if (!s.name || !s.body) continue
|
|
3059
|
+
const header = `---\nname: ${s.name}\ndescription: ${s.description || s.name}\n---\n\n`
|
|
3060
|
+
const f = join(agentsDir, `${s.name}.md`)
|
|
3061
|
+
writeFileSync(f, header + s.body, 'utf8')
|
|
3062
|
+
written.push(f)
|
|
3063
|
+
}
|
|
3064
|
+
|
|
3065
|
+
// ── 3. Task-specific active-agent.md — always written, deleted on done ────
|
|
3066
|
+
// Uses DB subagent body as template if one matches the role; falls back to
|
|
3067
|
+
// built-in ROLE_RULES template with task-specific data injected.
|
|
3068
|
+
if (role) {
|
|
3069
|
+
const dbMatch = dbSubagents.find(s => s.name === `${role}-agent` || s.name === role)
|
|
3070
|
+
let agentBody
|
|
3071
|
+
if (dbMatch?.body) {
|
|
3072
|
+
// Admin wrote a custom body — inject task data into placeholder tokens
|
|
3073
|
+
agentBody = dbMatch.body
|
|
3074
|
+
.replace(/\{\{taskId\}\}/g, taskId)
|
|
3075
|
+
.replace(/\{\{taskKey\}\}/g, taskKey)
|
|
3076
|
+
.replace(/\{\{taskTitle\}\}/g, taskTitle)
|
|
3077
|
+
.replace(/\{\{claimedFiles\}\}/g, claimedFiles.map(f => `- \`${f}\``).join('\n') || '(none)')
|
|
3078
|
+
} else {
|
|
3079
|
+
// Built-in fallback template
|
|
3080
|
+
const fileLine = claimedFiles.length
|
|
3081
|
+
? claimedFiles.map(f => `- \`${f}\``).join('\n')
|
|
3082
|
+
: '- (no files claimed yet — call claim_files first)'
|
|
3083
|
+
const scoutLine = hasScout ? '✅ Scout report available — call `get_agent_context` to read it.' : '⚠️ No scout report yet.'
|
|
3084
|
+
const decLine = hasDec ? '✅ Execution plan exists — follow the decomposition order.' : ''
|
|
3085
|
+
|
|
3086
|
+
const BUILT_IN = {
|
|
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`,
|
|
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`,
|
|
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`,
|
|
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`,
|
|
3091
|
+
}
|
|
3092
|
+
agentBody = BUILT_IN[role] || `# ${role} Agent — ${taskKey}\n\n**Task ID:** \`${taskId}\`\n**Task:** ${taskTitle}\n`
|
|
3093
|
+
}
|
|
3094
|
+
|
|
3095
|
+
const activeAgentContent = `---\nname: active-agent\ndescription: ${role} agent for ${taskKey} — auto-generated by InternalTool MCP on kickoff. Deleted when task is parked or done.\n---\n\n${agentBody}`
|
|
3096
|
+
writeFileSync(join(agentsDir, 'active-agent.md'), activeAgentContent, 'utf8')
|
|
3097
|
+
written.push(join(agentsDir, 'active-agent.md'))
|
|
3098
|
+
}
|
|
3099
|
+
|
|
3100
|
+
// ── 4. Custom role agents from project.agentConfig.roles (non-subagent) ───
|
|
3101
|
+
for (const cr of (cfg.roles || [])) {
|
|
3102
|
+
if (!cr.name || dbSubagents.some(s => s.name === `${cr.name}-agent`)) continue
|
|
3103
|
+
const content = `---\nname: ${cr.name}-agent\ndescription: ${cr.label || cr.name}${cr.promptHint ? ' — ' + cr.promptHint.slice(0, 80) : ''}\n---\n\n# ${cr.label || cr.name} Agent\n\n**Role:** \`${cr.name}\`\n${cr.promptHint ? `\n## Instructions\n${cr.promptHint}\n` : ''}${cr.allowedTools?.length ? `\n## Allowed tools\n${cr.allowedTools.map(t => `- ${t}`).join('\n')}\n` : ''}`
|
|
3104
|
+
const f = join(agentsDir, `${cr.name}-agent.md`)
|
|
3105
|
+
writeFileSync(f, content, 'utf8')
|
|
3106
|
+
written.push(f)
|
|
3107
|
+
}
|
|
3108
|
+
|
|
3109
|
+
// ── 5. Skills from DB → .cursor/skills/<name>.md ──────────────────────────
|
|
3110
|
+
// If DB is empty, write built-in defaults so Cursor always has something useful.
|
|
3111
|
+
const dbSkills = cfg.skills || []
|
|
3112
|
+
if (dbSkills.length > 0) {
|
|
3113
|
+
for (const s of dbSkills) {
|
|
3114
|
+
if (!s.name || !s.body) continue
|
|
3115
|
+
const header = `---\nname: ${s.name}\ndescription: ${s.description || s.name}\n---\n\n`
|
|
3116
|
+
const f = join(skillsDir, `${s.name}.md`)
|
|
3117
|
+
writeFileSync(f, header + s.body, 'utf8')
|
|
3118
|
+
written.push(f)
|
|
3119
|
+
}
|
|
3120
|
+
} else {
|
|
3121
|
+
// Built-in default skills
|
|
3122
|
+
const defaults = [
|
|
3123
|
+
{
|
|
3124
|
+
name: 'scout-codebase',
|
|
3125
|
+
description: 'Read every source file and save a scout report to InternalTool.',
|
|
3126
|
+
body: `1. Read all source files: routes, models, middleware, utils, tests, README, .cursorrules\n2. Write a report: Auth Flow, Data Model, API Surface, Utilities, Test Coverage, Gaps & Risks\n3. Call \`scout_task\` with taskId \`${taskId}\` confirmed=false (preview), then confirmed=true + report\n\n**READ ONLY — do not modify any file.**`,
|
|
3127
|
+
},
|
|
3128
|
+
{
|
|
3129
|
+
name: 'run-tests',
|
|
3130
|
+
description: 'Run the test suite and fix any failures before marking done.',
|
|
3131
|
+
body: `1. Run \`npm test\`\n2. Report: passed / failed / skipped\n3. For each failure: identify root cause, fix source (not the test)\n4. Re-run until 0 failures\n5. Do NOT mark done until tests are green`,
|
|
3132
|
+
},
|
|
3133
|
+
]
|
|
3134
|
+
for (const s of defaults) {
|
|
3135
|
+
const content = `---\nname: ${s.name}\ndescription: ${s.description}\n---\n\n${s.body}\n`
|
|
3136
|
+
const f = join(skillsDir, `${s.name}.md`)
|
|
3137
|
+
writeFileSync(f, content, 'utf8')
|
|
3138
|
+
written.push(f)
|
|
3139
|
+
}
|
|
3140
|
+
}
|
|
3141
|
+
|
|
3142
|
+
// ── 6. Command — start-task pre-filled with this task ─────────────────────
|
|
3143
|
+
const startCmd = `---\nname: start-task\ndescription: Start a task from InternalTool — brief, role rules, moves to In Progress.\n---\n\nDefault task: \`${taskId}\` (${taskKey} — ${taskTitle})\n\n1. Ask the user to confirm the task ID or use the default above\n2. Call \`kickoff_task\` with confirmed=false to show the brief\n3. Present: role, description, claimedFiles, cursorRules\n4. Ask "Ready to confirm and move to In Progress?"\n5. If yes, call \`kickoff_task\` again with confirmed=true\n`
|
|
3144
|
+
writeFileSync(join(commandsDir, 'start-task.md'), startCmd, 'utf8')
|
|
3145
|
+
written.push(join(commandsDir, 'start-task.md'))
|
|
3146
|
+
|
|
3147
|
+
return { repoRoot, written }
|
|
3148
|
+
} catch {
|
|
3149
|
+
return null
|
|
3150
|
+
}
|
|
3151
|
+
}
|
|
3152
|
+
|
|
3153
|
+
/**
|
|
3154
|
+
* Delete task-specific workspace files written at kickoff.
|
|
3155
|
+
* Project-level rules (.cursor/rules/<project-rule>.mdc) and skills are kept.
|
|
3156
|
+
* Only the active-agent and start-task command are removed (they are task-scoped).
|
|
3157
|
+
*/
|
|
3158
|
+
function deleteCursorWorkspace(role, startPath) {
|
|
3159
|
+
const deleted = []
|
|
3160
|
+
try {
|
|
3161
|
+
const repoRoot = findRepoRoot(startPath)
|
|
3162
|
+
if (!repoRoot) return deleted
|
|
3163
|
+
const toDelete = [
|
|
3164
|
+
join(repoRoot, '.cursor', 'agents', 'active-agent.md'),
|
|
3165
|
+
join(repoRoot, '.cursor', 'commands', 'start-task.md'),
|
|
3166
|
+
]
|
|
3167
|
+
for (const f of toDelete) {
|
|
3168
|
+
try {
|
|
3169
|
+
if (existsSync(f)) { unlinkSync(f); deleted.push(f) }
|
|
3170
|
+
} catch { /* non-fatal */ }
|
|
3171
|
+
}
|
|
3172
|
+
} catch { /* non-fatal */ }
|
|
3173
|
+
return deleted
|
|
3174
|
+
}
|
|
3175
|
+
|
|
3176
|
+
/**
|
|
3177
|
+
* Record that a specific MCP tool was called for a task.
|
|
3178
|
+
* 1. Updates agentWorkspace.lastActivityAt + lastTool (fast summary for overview card)
|
|
3179
|
+
* 2. Appends to agentSessionLog (the ordered call history shown in Session tab)
|
|
3180
|
+
* Fire-and-forget: never blocks the tool response.
|
|
3181
|
+
*/
|
|
3182
|
+
function trackTaskActivity(taskId, toolName, opts = {}) {
|
|
3183
|
+
if (!taskId) return
|
|
3184
|
+
// Fast summary update
|
|
3185
|
+
api.patch(`/api/tasks/${taskId}`, {
|
|
3186
|
+
agentWorkspace: {
|
|
3187
|
+
lastActivityAt: new Date().toISOString(),
|
|
3188
|
+
lastTool: toolName,
|
|
3189
|
+
_incCallCount: true,
|
|
3190
|
+
...(opts.role ? { role: opts.role } : {}),
|
|
3191
|
+
},
|
|
3192
|
+
}).catch(() => {})
|
|
3193
|
+
// Ordered session log entry
|
|
3194
|
+
api.post(`/api/tasks/${taskId}/session/log`, {
|
|
3195
|
+
type: 'tool',
|
|
3196
|
+
name: toolName,
|
|
3197
|
+
role: opts.role || null,
|
|
3198
|
+
summary: opts.summary || '',
|
|
3199
|
+
}).catch(() => {})
|
|
3200
|
+
}
|
|
3201
|
+
|
|
2827
3202
|
/** Delete the task-specific cursor rules file when work is complete. */
|
|
2828
3203
|
function deleteCursorRulesFile(taskKey, startPath) {
|
|
2829
3204
|
try {
|
|
@@ -3278,6 +3653,7 @@ Use this when returning to a task after a break, or when Claude needs the full p
|
|
|
3278
3653
|
repoPath: z.string().optional().describe('Absolute path to the local git repo (defaults to MCP process working directory)'),
|
|
3279
3654
|
},
|
|
3280
3655
|
async ({ taskId, repoPath }) => {
|
|
3656
|
+
trackTaskActivity(taskId, 'get_task_context')
|
|
3281
3657
|
let taskRes, activityRes
|
|
3282
3658
|
try {
|
|
3283
3659
|
[taskRes, activityRes] = await Promise.all([
|
|
@@ -3414,7 +3790,9 @@ If you have uncommitted tracked changes, it will tell you exactly what to do bef
|
|
|
3414
3790
|
// ── Approval gate check ───────────────────────────────────────────────────
|
|
3415
3791
|
// The server blocks non-admins from moving todo → in_progress without approval.
|
|
3416
3792
|
// Detect this early and guide the developer instead of failing silently after branch creation.
|
|
3793
|
+
const pendingApv2 = (task.approvals || []).find(a => a.state === 'pending') || null
|
|
3417
3794
|
const hasApprovedApv2 = (task.approvals || []).some(a => a.state === 'approved')
|
|
3795
|
+
const approvalState = pendingApv2 ? 'pending' : hasApprovedApv2 ? 'approved' : 'none'
|
|
3418
3796
|
const PLANNING_COLS = ['backlog', 'todo']
|
|
3419
3797
|
const needsApproval = PLANNING_COLS.includes(task.column) && !hasApprovedApv2
|
|
3420
3798
|
if (needsApproval && !confirmed) {
|
package/package.json
CHANGED